-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Components: Wire up reduxified country states #8414
Conversation
My proposed changes don't work yet unfortunately. @gwwar can you point me to what I'm doing wrong?
I wrapped It also seems that the fallback for when there are no states requires the query component to be called before checking for the existence of states, so I can't really use it "inline"? |
Almost! If we use the higher-order component localize to wrap StateSelect, translate is available as a component property. So instead of { this.translate( 'foo' ); } Usage becomes: const { translate } = this.props;
//...
{ translate( 'foo' ) } |
mixins: [ observe( 'statesList' ) ], | ||
|
||
recordStateSelectClick: function() { | ||
recordStateSelectClick() { | ||
if ( this.props.eventFormName ) { | ||
analytics.ga.recordEvent( 'Upgrades', `Clicked ${ this.props.eventFormName } State Select` ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is a connected component, let's use the recordGoogleEvent redux action . This makes it a lot easier to write any component tests.
So:
analytics.ga.recordEvent( 'Upgrades', `Clicked ${ this.props.eventFormName } State Select` );
turns into
const { recordGoogleEvent } = this.props;
recordGoogleEvent( 'Upgrades', `Clicked ${ this.props.eventFormName } State Select` );
} | ||
|
||
query() { | ||
return this.props.countryCode && <QueryCountryStates countryCode={ this.props.countryCode } />; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usage seems a bit awkward. Maybe let's update QueryCountryStates to do a no-op if countryCode is falsey?
const classes = classNames( this.props.additionalClasses, 'state' ), | ||
statesList = this.props.statesList.getByCountry( this.props.countryCode ); | ||
let options = []; | ||
this.query(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's have the QueryComponent inline instead.
* Uses Redux based Google Analytics. * Moves `QueryCountryStates` inline. * Removes `lib/states-list`.
import React, { Component, PropTypes } from 'react'; | ||
import classNames from 'classnames'; | ||
import { connect } from 'react-redux'; | ||
import isEmpty from 'lodash/isEmpty'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As of #6539, we encourage importing from the root Lodash module directly:
import { isEmpty } from 'lodash';
analytics.ga.recordEvent( 'Upgrades', `Clicked ${ this.props.eventFormName } State Select` ); | ||
recordStateSelectClick( eventFormName ) { | ||
if ( eventFormName ) { | ||
recordGoogleEvent( 'Upgrades', `Clicked ${ eventFormName } State Select` ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're not dispatching an action here, only calling an action creator. The action creator will return a thunk, but you need to use react-redux
's connect
mapDispatchToProps
to pass the action creator as a wrapped dispatch function prop into the component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry, forgot to add example connect code for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No reason to apologize, I should know better. I actually passed it to connect()
at one point but for some reason decided to remove it again
value={ this.props.value } | ||
disabled={ this.props.disabled } | ||
onChange={ this.props.onChange } | ||
onClick={ this.recordStateSelectClick.bind( this, this.props.eventFormName ) } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've enabled the react/jsx-no-bind
ESLint rule which this violates because bind
will always create a new function, thereby preventing any possible benefits of pure render of a child component. Instead, you should bind components in the constructor:
constructor() {
super( ...arguments );
this.recordStateSelectClick = this.recordStateSelectClick.bind( this );
}
recordStateSelectClick() {
const { eventForm } = this.props;
// ...
}
... or stage 1 class instance fields:
recordStateSelectClick = () => {
const { eventForm } = this.props;
// ...
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My implementation currently looks like this:
recordStateSelectClick() {
const { eventFormName, recordGoogleEvent } = this.props;
if ( eventFormName ) {
recordGoogleEvent( 'Upgrades', `Clicked ${ eventFormName } State Select` );
}
}
This however gives me a no-shadow
es-lint warning since I'm already importing recordGoogleEvent
in the upper scope. Is there a better way to mitigate that than using this.props.recordGoogleEvent()
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm generally lazy and do this.props.recordGoogleEvent()
but there's nothing stopping us from making another variable.
const { eventFromName, recordGoogleEvent: recordEvent } = this.props;
if ( eventFormName ) {
recordEvent( 'Upgrades', `Clicked ${ eventFormName } State Select` );
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha! I tried const { eventFromName, recordGoogleEvent as recordEvent } = this.props;
. Learned something yet again :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea it can be confusing since the import syntax is similar but not quite the same (i.e. import { recordGoogleEvent as recordEvent } from '';
would be correct).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that's what I thought of :)
{ isEmpty( this.props.countryStates ) | ||
? <Input ref="input" { ...this.props } /> | ||
: <div className={ classes }> | ||
<FormLabel htmlFor={ this.props.name }>{ this.props.label }</FormLabel> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could probably avoid the htmlFor
(which I don't think works anyways, since this assumes an ID on the <select>
) by including the <FormSelect />
render as a child of the <FormLabel />
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems easier to add the id. Nesting it would require style updates since the bottom margin of the label
element doesn't work with the select anymore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems easier to add the id.
Not super easy in React, since multiple instances of the component could be present on the page, and the ID needs to be unique. So at a minimum it becomes something like:
class MyComponent extends Component {
static instances = 0;
componentWillMount() {
this.instance = ++this.constructor.instances;
}
render() {
return <input id={ this.instance } />
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like it is assumed that there will ever be only one instance of each form field: https://github.com/Automattic/wp-calypso/blob/master/client/my-sites/upgrades/components/form/input.jsx#L81
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, in that case the name
would need to be a required prop, otherwise the htmlFor
will be ineffective if omitted. Seems like a nuisance from an implementer's perspective. I personally think we should go the nesting route even if it requires style updates.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Real-world example of unique IDs: https://github.com/Automattic/wp-calypso/blob/master/client/components/spinner/index.jsx
We should plan to move |
|
Confirmed that the select works well during the domain registration step 👍 |
module.exports = connect( | ||
( state, { countryCode } ) => ( { | ||
countryStates: countryCode ? getCountryStates( state, countryCode ) : [] | ||
} ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't forget that we can use fancy es6/es2015 features because we use babel. In this case let's use export over module.exports
export default connect(
( state, { countryCode } ) => {
return countryStates: countryCode ? getCountryStates( state, countryCode ) : []
},
{ recordGoogleEvent } // this can be an object
)( localize( StateSelect ) );
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The height issue might be the same that this PR attempts to address: #8470. |
Thanks for the ping @tyxla. Let's resolve the styling issue in 8470 then |
Tested successfully when editing existing contact information. Final sign-off? |
Looks good! |
Moves away from
lib/states-list
to usingcountryStates
built on Redux.To test:
My Sites
.Domains
, clickAdd
.Select
for any domain to enter the checkout process.Domain Contact Information
form, selectUnited States
in the country dropdown. TheState
input field should have been replaced with a dropdown of states.See #8202.
Fixes #7922.